package com.atguigu.tingshu.user.service.impl;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.WxMaUserService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.album.client.AlbumInfoFeignClient;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.common.rabbit.service.RabbitService;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.common.util.MongoUtil;
import com.atguigu.tingshu.model.user.*;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper;
import com.atguigu.tingshu.user.mapper.UserPaidTrackMapper;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.album.TrackListVo;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.atguigu.tingshu.vo.user.UserCollectVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Service;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;

	@Autowired
	private UserInfoMapper userInfoMapper;

    @Autowired
    private WxMaService wxMaService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RabbitService rabbitService;

    @Autowired
    private RsaSigner rsaSigner;

    @Autowired
    private UserPaidTrackMapper userPaidTrackMapper;

    @Autowired
    private UserPaidAlbumMapper userPaidAlbumMapper;
    /**
     * 微信登录方法，处理用户通过微信授权码进行登录的逻辑。
     *
     * @param code 微信登录时返回的授权码（code），用于获取用户的唯一标识（openId）
     * @return 返回一个包含token和refreshToken的HashMap对象，用于前端验证用户身份
     * @throws RuntimeException 如果授权码为空，则抛出运行时异常
     * @throws GuiguException 如果openId为空或微信内部处理异常，则抛出自定义异常
     */
    @Override
    public HashMap<String, Object> wxLogin(String code) {
        String openId = "";

        // 校验code码是否存在
        if (StringUtils.isEmpty(code)) {
            log.error("微信授权登录失败：code为空");
            throw new RuntimeException("微信授权登录失败：code为空");
        }

        // 获取微信登录的客户端服务
        WxMaUserService userService = wxMaService.getUserService();

        try {
            // 使用授权码换取会话信息（包括openId等）
            WxMaJscode2SessionResult sessionInfo = userService.getSessionInfo(code);
            openId = sessionInfo.getOpenid(); // 用户身份的唯一标识

            if (StringUtils.isEmpty(openId)) {
                throw new GuiguException(201, "openId不存在,登录失败");
            }
        } catch (WxErrorException e) {
            throw new GuiguException(201, "微信内部处理异常，登录失败");
        }

        // 检查Redis中是否存在未过期的refreshToken
        String refreshTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + "refresh:" + openId;
        String fromRedisJwt = redisTemplate.opsForValue().get(refreshTokenKey);

        if (!StringUtils.isEmpty(fromRedisJwt)) {
            // 如果存在未过期的refreshToken，直接返回该令牌
            HashMap<String, Object> map = new HashMap<>();
            map.put("token", fromRedisJwt);
            map.put("refreshToken", fromRedisJwt);
            return map;
        }

        // 查询数据库，检查用户是否已存在
        LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserInfo::getWxOpenId, openId);
        UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);

        // 如果用户不存在，则注册新用户
        if (null == userInfo) {
            userInfo = new UserInfo();
            userInfo.setWxOpenId(openId);
            userInfo.setNickname("用户-" + System.currentTimeMillis());
            userInfo.setAvatarUrl("https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif"); // 初始化用户头像
            userInfo.setIsVip(0);
            userInfo.setVipExpireTime(new Date());
            userInfo.setGender(0);
            userInfo.setIntro("简介");

            int insert = userInfoMapper.insert(userInfo);
            log.info("注册用户信息：{}", insert > 0 ? "success" : "fail");

            // 异步发送消息初始化用户账户信息
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_USER_REGISTER, userInfo.getId().toString());
        }

        // 生成自定义登录信息（JWT令牌）
        String jsonWebToken = getJsonWebToken(openId, userInfo.getId());

        // 将JWT令牌存储到Redis中
        String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;
        redisTemplate.opsForValue().set(accessTokenKey, jsonWebToken, 10, TimeUnit.DAYS);
        redisTemplate.opsForValue().set(refreshTokenKey, jsonWebToken, 30, TimeUnit.DAYS);

        // 返回自定义登录信息给前端
        HashMap<String, Object> map = new HashMap<>();
        map.put("token", jsonWebToken);   // token:前端用的key
        map.put("refreshToken", jsonWebToken);

        return map;
    }

    /**
     * 生成JSON Web Token（JWT）的方法。
     *
     * @param openId 用户身份的唯一标识
     * @param userId 用户ID
     * @return 返回生成的JWT令牌
     */
    private String getJsonWebToken(String openId, Long userId) {
        // 创建JWT载荷数据
        JSONObject payloadMap = new JSONObject();
        // 添加用户身份信息
        payloadMap.put("openId", openId);
        payloadMap.put("userId", userId);

        // 编码并生成JWT对象
        Jwt jwt = JwtHelper.encode(payloadMap.toString(), rsaSigner);

        // 获取JWT对象的编码字符串
        return jwt.getEncoded();
    }

    /**
     * 刷新令牌方法，用于处理用户刷新访问令牌的逻辑。
     *
     * @return 返回一个包含新token和refreshToken的HashMap对象
     * @throws GuiguException 如果refreshToken过期，则抛出自定义异常
     */
    @Override
    public HashMap<String, Object> refreshToken() {
        // 获取当前请求对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();

        // 获取请求头中的refreshToken
        String refreshToken = request.getHeader("token");

        // 验签并解析JWT载荷数据
        Jwt jwt = JwtHelper.decodeAndVerify(refreshToken, new RsaVerifier(PublicConstant.PUBLIC_KEY));
        String claims = jwt.getClaims();
        Map<String, Object> claimMap = JSONObject.parseObject(claims, Map.class);
        String openId = (String) claimMap.get("openId");
        Long userId = (Long) claimMap.get("userId");

        // 检查refreshToken是否过期
        String refreshTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + "refresh:" + openId;
        String dataFromRedis = redisTemplate.opsForValue().get(refreshTokenKey);

        if (!StringUtils.isEmpty(dataFromRedis)) {
            // 如果refreshToken未过期，生成新的访问令牌
            String jsonWebToken = getJsonWebToken(openId, userId);
            // 将新的访问令牌存储到Redis中
            String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;

            redisTemplate.opsForValue().set(accessTokenKey, jsonWebToken, 10, TimeUnit.DAYS);
            redisTemplate.opsForValue().set(refreshTokenKey, jsonWebToken, 30, TimeUnit.DAYS);

            HashMap<String, Object> map = new HashMap<>();
            map.put("token", jsonWebToken);
            map.put("refreshToken", jsonWebToken);
            return map;
        }

        // 如果refreshToken过期，提示用户重新登录
        throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
    }

    /**
     * 获取用户信息
     * 此方法用于从认证中心的ThreadLocal中获取当前用户的ID，然后查询用户表以获取用户详细信息，
     * 并将这些信息封装到UserInfoVo对象中返回如果用户信息不存在，则抛出异常
     *
     * @return UserInfoVo对象，包含用户信息
     * @throws GuiguException 如果用户信息不存在，则抛出此异常
     */
    @Override
    public UserInfoVo getUserInfo(Long userId) {

        if (null == userId){
            userId = AuthContextHolder.getUserId();
        }
        // 1.查询用户表
        UserInfo userInfo = userInfoMapper.selectById(userId);

        if (null == userInfo) {
            throw new GuiguException(201, "用户信息不存在");
        }

        // 2.将UserInfo转成UserInfoVo对象返回
        UserInfoVo userInfoVo = new UserInfoVo();
        BeanUtils.copyProperties(userInfo, userInfoVo); // 两个对象的属性名 两个对象的属性类型是否一致

        return userInfoVo;
    }

    @Override
    public Boolean updateUser(UserInfoVo userInfoVo) {
        UserInfo userInfo = new UserInfo();
        BeanUtils.copyProperties(userInfoVo, userInfo);
        int update = userInfoMapper.updateById(userInfo);
        return update > 0;
    }

    @Override
    public Map<Long, String> getUserPaidAlbumTrackList(Long userId, Long albumId) {
        LambdaQueryWrapper<UserPaidTrack> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(UserPaidTrack::getUserId, userId);
        wrapper.eq(UserPaidTrack::getAlbumId, albumId);
        wrapper.eq(UserPaidTrack::getIsDeleted, 0);

        // 1.查询该用户购买过的专辑下的声音列表
        List<UserPaidTrack> userPaidTracks = userPaidTrackMapper.selectList(wrapper);


        if (CollectionUtils.isEmpty(userPaidTracks)) {
            return new HashMap<>();
        }
        // 2.将集合转成Map
        Map<Long, String> map = userPaidTracks.stream().collect(Collectors.toMap(UserPaidTrack::getTrackId, v -> "1"));
        return map;
    }

    @Override
    public Boolean getUserPaidAlbum(Long userId, Long albumId) {
        LambdaQueryWrapper<UserPaidAlbum> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(UserPaidAlbum::getUserId, userId);
        wrapper.eq(UserPaidAlbum::getAlbumId, albumId);
        wrapper.eq(UserPaidAlbum::getIsDeleted, 0);
        UserPaidAlbum userPaidAlbum = userPaidAlbumMapper.selectOne(wrapper);
        return userPaidAlbum != null;
    }

    /**
     * 收藏或取消收藏操作
     *
     * @param trackId 音轨ID，用于标识待收藏的音轨
     * @return 返回一个布尔值，表示收藏操作的结果，true表示成功收藏，false表示成功取消收藏
     */
    @Override
    public Boolean collect(Long trackId) {
        // 获取当前用户ID
        Long userId = AuthContextHolder.getUserId();

        // 1.构建条件对象，用于查询用户是否已收藏该音轨
        Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);

        // 2.构建一个查询对象，封装查询条件
        Query query = new Query(criteria);

        // 3.查询用户收藏记录，以确定是否已经收藏
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);
        long count = mongoTemplate.count(query, UserCollect.class, collectionName);

        // 4.判断用户是否已收藏该音轨
        if (count == 0) {
            // 如果未收藏，则执行收藏操作
            UserCollect userCollect = new UserCollect();
            userCollect.setId(ObjectId.get().toString());
            userCollect.setUserId(userId);
            userCollect.setTrackId(trackId);
            userCollect.setCreateTime(new Date());
            mongoTemplate.save(userCollect, collectionName);

            // 收藏后，发送消息更新音轨的收藏量统计
            TrackStatMqVo trackStatMqVo = prepareCollectMsgDto(trackId, 1);
            rabbitService.sendMessage(MqConst.EXCHANGE_TRACK, MqConst.ROUTING_TRACK_STAT_UPDATE, JSONObject.toJSONString(trackStatMqVo));

            return true;
        } else {
            // 如果已收藏，则执行取消收藏操作
            mongoTemplate.remove(query, collectionName);

            // 取消收藏后，发送消息更新音轨的收藏量统计
            TrackStatMqVo trackStatMqVo = prepareCollectMsgDto(trackId, -1);
            rabbitService.sendMessage(MqConst.EXCHANGE_TRACK, MqConst.ROUTING_TRACK_STAT_UPDATE, JSONObject.toJSONString(trackStatMqVo));

            return false;
        }
    }


    /**
     * 取消收藏指定的声音
     *
     * @param trackId 轨迹ID
     * @return 取消成功，则返回true；否则返回false
     */
    @Override
    public Boolean isCollect(Long trackId) {

        try {
            // 获取当前用户ID
            Long userId = AuthContextHolder.getUserId();

            // 1.构建条件对象
            // 根据用户ID和轨迹ID构建查询条件，用于检查该用户是否收藏了指定声音
            Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);

            // 2.构建一个查询对象
            // 使用之前构建的条件对象创建一个查询对象，以便在MongoDB中执行查询
            Query query = new Query(criteria);

            // 3.查询
            // 根据用户ID获取用户收藏的MongoDB集合名称
            String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);
            // 使用mongoTemplate执行查询，并统计符合条件的文档数量
            long count = mongoTemplate.count(query, UserCollect.class, collectionName);

            // 根据查询结果判断用户是否收藏了指定声音
            // 如果已收藏，则执行取消收藏操作
            mongoTemplate.remove(query, collectionName);

            // 取消收藏后，发送消息更新音轨的收藏量统计
            TrackStatMqVo trackStatMqVo = prepareCollectMsgDto(trackId, -1);
            rabbitService.sendMessage(MqConst.EXCHANGE_TRACK, MqConst.ROUTING_TRACK_STAT_UPDATE, JSONObject.toJSONString(trackStatMqVo));
            return true;
        } catch (Exception e){
            return false;
        }


    }


    /**
     * 判断用户是否订阅了指定的专辑
     *
     * @param albumId 专辑ID，用于标识特定的专辑
     * @return 如果用户订阅了指定的专辑，则返回true；否则返回false
     */
    @Override
    public Boolean isSubscribe(Long albumId) {
        // 获取当前用户的ID
        Long userId = AuthContextHolder.getUserId();

        // 1.构建条件对象
        // 创建一个条件对象，用于指定查询的用户ID和专辑ID
        Criteria criteria = Criteria.where("userId").is(userId).and("albumId").is(albumId);

        // 2.构建一个查询对象
        // 使用之前创建的条件对象构建一个查询对象，以便在MongoDB中执行查询
        Query query = new Query(criteria);

        // 3.查询
        // 根据用户ID获取用户订阅的集合名称，并使用mongoTemplate执行查询并计数
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_SUBSCRIBE, userId);
        long count = mongoTemplate.count(query, UserSubscribe.class, collectionName);

        // 根据查询结果判断用户是否订阅了指定的专辑
        return count > 0;
    }


    /**
     * 根据用户ID查询用户收藏信息分页数据
     *
     * @param page 分页对象，包含当前页码和每页大小
     * @param userId 用户ID，用于查询该用户的收藏信息
     * @return 返回填充了用户收藏信息的分页对象
     */
    @Override
    public IPage<UserCollectVo>  findUserCollectPage(IPage<UserCollectVo> page, Long userId) {

        // 1.构建条件对象
        Criteria criteria = Criteria.where("userId").is(userId);

        // 2.构建查询对象
        Query query = new Query(criteria);
        // 3.分页+排序
        // from:(pageNo-1*pageSize)
        long from = (page.getCurrent() - 1) * page.getSize();
        Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
        PageRequest pageRequest = PageRequest.of((int) from, (int) page.getSize(), sort);

        query.with(pageRequest);

        // 4.查询（查询满足条件的总记录数）
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);

        long total = mongoTemplate.count(query.limit(-1), UserCollect.class, collectionName);

        // 5.查询数据
        List<UserCollect> userCollects = mongoTemplate.find(query, UserCollect.class, collectionName);

        List<UserCollectVo> userCollectVoList = new ArrayList<>();
        userCollects.forEach(
                userCollect -> {
                    UserCollectVo userCollectVo = new UserCollectVo();
                    BeanUtils.copyProperties(userCollect, userCollectVo);
                    userCollectVoList.add(userCollectVo);
                }
        );
        // 6.获取收藏的声音id集合
        List<Long> collectTrackIds = userCollects.stream().map(UserCollect::getTrackId).collect(Collectors.toList());

        // 7.查询数据库获取到标题以及封面专辑id
        if (!CollectionUtils.isEmpty(collectTrackIds)) {
            Result<List<TrackListVo>> trackInfoVoResult = albumInfoFeignClient.getTrackInfoList(collectTrackIds);
            List<TrackListVo> trackListVoData = trackInfoVoResult.getData();
            if (CollectionUtils.isEmpty(trackListVoData)) {
                throw new GuiguException(201, "远程查询声音列表失败");
            }

            // 8.赋值
            Map<Long, UserCollectVo> collect = userCollectVoList
                    .stream()
                    .collect(Collectors.toMap(UserCollectVo::getTrackId, v -> v));
            List<UserCollectVo> userCollectVoLists = new ArrayList<>();
            trackListVoData.forEach(
                    trackListVo -> {
                        UserCollectVo userCollectVo = new UserCollectVo();
                        userCollectVo.setAlbumId(trackListVo.getAlbumId());
                        userCollectVo.setTrackId(trackListVo.getTrackId());
                        userCollectVo.setTrackTitle(trackListVo.getTrackTitle());
                        userCollectVo.setCoverUrl(trackListVo.getCoverUrl());
                        userCollectVo.setCreateTime(collect.get(trackListVo.getTrackId()).getCreateTime());
                        userCollectVoLists.add(userCollectVo);
                    }
            );
            return page.setRecords(userCollectVoLists).setTotal(total);
        }
        return page;
    }


    /**
     * 准备收集消息DTO
     * 该方法用于创建并初始化一个TrackStatMqVo对象，用于声音统计信息的收集
     *
     * @param trackId 声音ID，表示需要统计的声音
     * @param count 统计数量，表示声音被收集的次数
     * @return 返回一个初始化完毕的TrackStatMqVo对象，用于发送到消息队列进行收集统计
     */
    private TrackStatMqVo prepareCollectMsgDto(Long trackId, Integer count) {

        // 创建一个TrackStatMqVo对象用于封装声音统计信息
        TrackStatMqVo trackStatMqVo = new TrackStatMqVo();

        // 设置业务编号，使用UUID确保唯一性，去除"-"以便于传输和存储
        trackStatMqVo.setBusinessNo(UUID.randomUUID().toString().replaceAll("-", ""));

        // 设置声音ID，用于标识统计信息所属的声音
        trackStatMqVo.setTrackId(trackId);

        // 设置统计类型，此处统计类型为收集操作，通过常量确保代码可读性和一致性
        trackStatMqVo.setStatType(SystemConstant.TRACK_STAT_COLLECT);

        // 设置统计数量，表示本次收集操作收集的声音次数
        trackStatMqVo.setCount(count);

        // 返回初始化完毕的TrackStatMqVo对象
        return trackStatMqVo;
    }

}
